package com.xrui.hbase.model;

import com.google.common.base.Preconditions;

import java.util.*;

/**
 * This class represents an encapsulation of the fields of a record.
 * <p>
 * <p>RecordFields has one build method, {@link #builder()}.</p>
 * <p>RecordFields consist of an ordered series of FieldKey</p>
 * <p>For more information on these field keys, see {@link FieldKey}.</p>
 */
public class RecordFields {
    /**
     * Field key and value mapping
     */
    private final Map<FieldKey<?>, Object> fields;

    private RecordFields(Map<FieldKey<?>, Object> fields) {
        Preconditions.checkNotNull(fields);
        this.fields = fields;
    }

    /**
     * Returns the value of a specified FieldKey.
     *
     * @param key of the field
     * @param <T> type of the field value
     * @return the value of field
     */
    public <T> T get(FieldKey<T> key) {
        return key.getType().cast(fields.get(key));
    }

    /**
     * Returns the value of a specified FieldKey name
     *
     * @param name of the field
     * @param <T>  type of the field value
     * @return the value of field
     * @throws NoSuchFieldException when no such field with the name
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String name) throws NoSuchFieldException {
        for (FieldKey<?> fieldKey : fields.keySet()) {
            if (Objects.equals(fieldKey.getName(), name)) {
                return get((FieldKey<T>) fieldKey);
            }
        }
        throw new NoSuchFieldException(String.format("Field with name %s not exists", name));
    }

    /**
     * Returns the value of a specified FieldKey name and value type
     *
     * @param name of the field
     * @param type of the field value
     * @param <T>  type of the field value
     * @return the value of field
     * @throws NoSuchFieldException when no such field with the name
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String name, Class<T> type) throws NoSuchFieldException {
        return get((FieldKey<T>) FieldKey.create(name, type));
    }

    /**
     * Returns {@code true} if the field with specified name exists
     * and {@code false} otherwise.
     *
     * @param name of the field
     * @return {@code true} if the field with specified name exists
     */
    public boolean containsField(String name) {
        for (FieldKey<?> fieldKey : fields.keySet()) {
            if (Objects.equals(fieldKey.getName(), name)) {
                return true;
            }
        }
        return false;
    }

    public Map<String, Object> toMap() {
        Map<String, Object> map = new LinkedHashMap<>();
        for(Map.Entry<FieldKey<?>, Object> entry : entrySet()) {
            map.put(entry.getKey().getName(), entry.getValue());
        }
        return map;
    }

    /**
     * Returns a new {@code Builder} of RecordFields
     *
     * @return a new {@code Builder}
     */
    public static Builder builder() {
        return Builder.newBuilder();
    }

    /**
     * Returns the number of field key-value mappings.
     *
     * @return the number of field key-value mappings
     */
    public int size() {
        return fields.size();
    }

    /**
     * Returns <tt>true</tt> if this contains no fields.
     *
     * @return <tt>true</tt> if this contains no fields
     */
    public boolean isEmpty() {
        return fields.isEmpty();
    }

    /**
     * Returns a {@link Set} view of the field keys contained.
     *
     * @return a set view of the keys contained
     */
    public Set<FieldKey<?>> keySet() {
        return fields.keySet();
    }

    /**
     * Returns a {@link Collection} view of the values contained.
     *
     * @return a collection view of the values contained
     */
    public Collection<Object> values() {
        return fields.values();
    }

    /**
     * Returns a {@link Set} view of the mappings contained.
     *
     * @return a set view of the mappings contained
     */
    public Set<Map.Entry<FieldKey<?>, Object>> entrySet() {
        return fields.entrySet();
    }

    @Override
    public String toString() {
        com.google.common.base.Objects.ToStringHelper helper =
            com.google.common.base.Objects.toStringHelper(RecordFields.class);

        for (Map.Entry<FieldKey<?>, Object> entry : entrySet()) {
            helper.add(entry.getKey().getName(), entry.getValue());
        }
        return helper.toString();
    }

    /**
     * Builds a new {@code RecordFields}
     */
    public static class Builder {
        public Map<FieldKey<?>, Object> values = new LinkedHashMap<>();

        public static Builder newBuilder() {
            return new Builder();
        }

        public Builder withFields(Map<FieldKey<?>, Object> fields) {
            values = fields;
            return this;
        }

        public Builder putFields(Map<FieldKey<?>, Object> fields) {
            values.putAll(fields);
            return this;
        }

        public <T> Builder putField(String key, T value) {
            Preconditions.checkNotNull(key);
            if (null != value) {
                values.put(FieldKey.create(key, value.getClass()), value);
            } else {
                values.put(FieldKey.create(key, null), null);
            }
            return this;
        }

        public <T> Builder putField(FieldKey<T> key, T value) {
            values.put(key, value);
            return this;
        }

        public RecordFields build() {
            return new RecordFields(values);
        }
    }
}
